﻿using System;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using CookComputing.XmlRpc;
using Microsoft.SharePoint;

namespace CKS.EBE.Linkbacks
{
    public class PingbackPinger : XmlRpcService
    {
        #region
        // When there are a lot of items in a list, SPList.Items.Add can get very slow. Using SPWeb.ProcessBatchData():
        private const string AddReceivedPingbackCamlBatchFormat =
            @"<?xml version=""1.0"" encoding=""UTF-8""?>
                    <ows:Batch>
                    <Method ID=""B1"">
                    <SetList>{0}</SetList>
                    <SetVar Name=""ID"">New</SetVar>
                    <SetVar Name=""Cmd"">Save</SetVar>
                    <SetVar Name=""urn:schemas-microsoft-com:office:office#PostTitle"">{1}</SetVar>
                    <SetVar Name=""urn:schemas-microsoft-com:office:office#LinkbackType"">Pingback</SetVar>
                    <SetVar Name=""urn:schemas-microsoft-com:office:office#BlogName"">{2}</SetVar>
                    <SetVar Name=""urn:schemas-microsoft-com:office:office#LinkingPost"">{3}, {4}</SetVar>
                    <SetVar Name=""urn:schemas-microsoft-com:office:office#LinkingPostExcerpt"">{5}</SetVar>
                    </Method>
                    </Batch>";  // {0} = Linkbacks Received list GUID, {1} = local post lookup value (ID number??),
                                // {2} = Name of "linking" blog, {3} = URL of linking post, {4} = title of linking post, 
                                // {5} = Excerpt from linking blog post

        private static readonly Regex _RegexHtml = new Regex(@"</?\w+((\s+\w+(\s*=\s*(?:"".*?""|'.*?'|[^'"">\s]+))?)+\s*|\s*)/?>", RegexOptions.Singleline | RegexOptions.Compiled);
        #endregion

        [XmlRpcMethod("pingback.ping")]
        public string Ping(string source, string destination)
        {
            var result = new PingbackResult();
            #region Check requirements
            try
            {
                if (BlogContext.Current.Settings.TrackbackReceiptEnabled == false)
                {
                    result.Message = "The owner of the blog has chosen not to receive linkbacks.";
                    result.Code = 0;
                }
                if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(destination))
                {
                    result.Message = "You are likely viewing this error code because you put the linkback ping URL into your Internet browser. Instead, you must enter the linkback ping URL in the appropriate field within your blogging application.";
                    result.Code = 0;
                }
            }
            catch (Exception ex)
            {
                Trace.Write("Error checking TrackbackReceiptEnabled: " + ex);
                result.Code = 0;
            }
            #endregion

            int localPostId = 0;
            if (result.Code == -1)
            {
                Uri destinationUri;
                if (Uri.TryCreate(destination, UriKind.Absolute, out destinationUri))
                {
                    if (destinationUri.Query.StartsWith("ID=", StringComparison.OrdinalIgnoreCase) &&
                        int.TryParse(destinationUri.Query.Substring(4), out localPostId))
                    {
                        // the url contains the post ID
                    }
                    else
                    {
                        //Get the post's ID based on the destination url from the custom URL Rewriting scheme.
                        SPListItem item = EBE.Helper.GetPostFromUrl(destination, SPContext.Current.Web);
                        if (item != null)
                        {
                            localPostId = item.ID;
                        }
                        else
                        {
                            result.Code = 32;
                            result.Message = "There is no post with the Url";
                        }
                    }
                }

                if (localPostId > 0)
                {
                    //Make sure we haven't already added this pingback
                    if (!AlreadyRegisteredPing(localPostId, source, destination, result))
                    {
                        ProcessPingback(source, destination, localPostId, result);
                    }
                }
                else
                {
                    result.Code = 32;
                    result.Message = "pingback not registered, no post found";
                }
            }

            string returnString = GenerateReturnString(result);
            return returnString;
        }

        private string GenerateReturnString(PingbackResult result)
        {
            Context.Response.ClearContent();
            Context.Response.ContentType = "text/xml";
            Context.Response.ContentEncoding = Encoding.UTF8;

            if (result.Code == -1)
            {
                // successfull
                return result.Message;
            }

            if (result.SpamDetected)
            {
                // hide the response to a spammer
                Context.Response.SuppressContent = true;
                Context.Response.Close();
            }

            // send error
            throw new XmlRpcFaultException(result.Code, result.Message);
        }

        /// <summary>
        /// process the request
        /// </summary>
        /// <param name="linkingPostUrl"></param>
        /// <param name="localPostUrl"></param>
        /// <param name="localPostId"></param>
        /// <param name="result"></param>
        private void ProcessPingback(string linkingPostUrl, string localPostUrl, int localPostId, PingbackResult result)
        {
            bool akismetApproved;
            string linkingPostTitle = null;
            bool spamDetected = Helper.DetectSpam(localPostUrl, linkingPostUrl, localPostId, Context.Request, out akismetApproved, ref linkingPostTitle);

            if (!spamDetected)
            {
                // for pingback, the linkinkgPostTitle must only contain plain text
                spamDetected = _RegexHtml.IsMatch(linkingPostTitle);
            }

            if (spamDetected)
            {
                Context.Response.SuppressContent = true;
                Context.Response.Close();
                result.SpamDetected = true;
            }
            try
            {
                SPSecurity.RunWithElevatedPrivileges(() => RegisterPingbackPing(localPostId, linkingPostUrl, linkingPostTitle, akismetApproved, result));
            }
            catch (Exception ex)
            {
                Trace.Write("Error registering Trackback ping: " + ex);
                result.Code = 0;
                result.Message = "An unknown error occurred";
            }
        }

        /// <summary>
        /// check if the linkingPost already pinged our post with the Id
        /// </summary>
        /// <param name="localPostId"></param>
        /// <param name="linkingPostUrl"></param>
        /// <param name="localPostUrl"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        private static bool AlreadyRegisteredPing(int localPostId, string linkingPostUrl, string localPostUrl, PingbackResult result)
        {
            bool alreadyPinged = false;
            SPSecurity.RunWithElevatedPrivileges(() =>
                                                     {
                                                         using (var site = new SPSite(SPContext.Current.Web.Url))
                                                         using (var web = site.OpenWeb())
                                                         {
                                                             var linkbacksReceivedList = web.Lists[Globals.LinkbacksReceivedListName];
                                                             // Query the list to see whether a trackback ping was already registered:
                                                             var duplicateQuery = new SPQuery {Query = string.Format(Globals.QueryForDuplicateReceivedLinkback, localPostId, HttpUtility.HtmlEncode(linkingPostUrl)), RowLimit = 1};
                                                             SPListItemCollection duplicateReceivedLinkbackItems = linkbacksReceivedList.GetItems(duplicateQuery);
                                                             if (duplicateReceivedLinkbackItems != null && duplicateReceivedLinkbackItems.Count > 0)
                                                             {
                                                                 
                                                                 result.Message = string.Format("You have already sent a trackback ping to {0} for the post at URL {1}", localPostUrl, linkingPostUrl);
                                                                 result.Code = 48;
                                                                 alreadyPinged = true;
                                                             }
                                                         }
                                                     });
            return alreadyPinged;
        }

        private static void RegisterPingbackPing(int localPostId, string linkingPostUrl, string linkingPostTitle, bool akismetApproved, PingbackResult result)
        {
            if (result.SpamDetected) return;
            using (var site = new SPSite(SPContext.Current.Web.Url))
            using (SPWeb web = site.OpenWeb()) // the web from the current context will not have elevated priveleges
            {
                web.AllowUnsafeUpdates = true;

                // Add a received trackback list item:
                SPList linkbacksReceivedList = null;
                try
                {
                    linkbacksReceivedList = web.Lists[Globals.LinkbacksReceivedListName];
                }
                catch (Exception ex)
                {
                    Trace.Write("Error getting Linkbacks Received List: " + ex);
                }

                if (linkbacksReceivedList == null)
                {
                    result.Code = 0;
                    result.Message = "The blog you are pinging is not configured correctly for receiving pingbacks.";
                }
                else
                {

                    string linkbacksReceivedListGuid = linkbacksReceivedList.ID.ToString();

                    string addTrackbackPingItemCaml = string.Format(AddReceivedPingbackCamlBatchFormat,
                                                                    linkbacksReceivedListGuid,
                                                                    localPostId,
                                                                    XmlEscape(linkingPostTitle),
                                                                    XmlEscape(linkingPostUrl),
                                                                    XmlEscape(linkingPostTitle),
                                                                    ""
                        );

                    string itemAdded = web.ProcessBatchData(addTrackbackPingItemCaml);

                    if (akismetApproved && !string.IsNullOrEmpty(BlogContext.Current.Settings.AkismetKey) && linkbacksReceivedList.EnableModeration ||
                    string.IsNullOrEmpty(BlogContext.Current.Settings.AkismetKey) && linkbacksReceivedList.EnableModeration)
                    {
                        // the linkback is akismet approved, or considered as spamfree
                        var query = new SPQuery {Query = string.Format(Globals.QueryForDuplicateReceivedLinkback, localPostId, XmlEscape(linkingPostUrl))};
                        SPListItemCollection items = linkbacksReceivedList.GetItems(query);
                        if (items.Count == 1)
                        {
                            // get item instead of modifying the one from the collection, because that way we can set the moderation status
                            var itemToApprove = linkbacksReceivedList.GetItemById(items[0].ID);
                            itemToApprove.ModerationInformation.Status = SPModerationStatusType.Approved;
                            itemToApprove.UpdateOverwriteVersion();
                        }
                    }

                    result.Message = "Your pingback has been accepted";

                    // TODO: Log any errors that may have occurred
                }
            }
        }

        private static string XmlEscape(string text)
        {
            return string.IsNullOrEmpty(text) ? string.Empty : HttpUtility.HtmlEncode(text);
        }
    }

    public interface IPingbackPing : IXmlRpcProxy
    {
        [return: XmlRpcReturnValue(Description = "Response")]
        [XmlRpcMethod("pingback.ping", Description = "Pingback implementation")]
        string Ping(string source, string destination);
    }
}
